from os import path
import smtplib, re, urllib2
from email.MIMEMultipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.MIMEImage import MIMEImage
from email.MIMEText import MIMEText

class PyMail:
	def __init__(self, subject='(No subject)', message='', html='', username=None, password=None):
		self.username = username
		self.password = password
		message = unicode(message)
		html = unicode(html)
		
		self.mail = MIMEMultipart('alternative')
		self.mail['subject'] = unicode(subject).strip()
		if message:		self.mail.attach(MIMEText(message, 'plain', 'utf-8'))
		if html:				self.mail.attach(MIMEText(html, 'html'))
		
		self.to = []
		self.cc = []
		self.bcc = []
		self.contentIDs = {}
	def createContentID(self, name, preview=True):
		while name in self.contentIDs:
			name = '_{name}'.format(name=name)
		contentID = re.sub(r'[\W_]+', '_', name)
		
		if not preview:
			self.contentIDs[name] = contentID
		return name
	def attach(self, *files, **namedFiles):
		def decideDataType(data):
			filename = None
			if isinstance(data, file):
				# File object
				filename = path.basename(data.name)
				data = data.read()
			elif isinstance(data, (str, unicode)):
				try:
					if path.exists(data):
						# Path to file
						filename = path.basename(data)
						data = open(data, 'rb').read()
					elif re.match('^(ht|f)tps?://', data):
						filename = path.basename(data)
						data = urllib2.urlopen(data).read()
				except TypeError:
					# Already a raw input stream?
					filename = 'file'
				except:
					# Not a well-formed URL, as expected
					pass
			
			return filename, data
		
		# First handle keyword arguments
		for item in namedFiles:
			# Get these explicit content-id's into our running list before anything else
			namedFiles[self.createContentID(item, False)] = decideDataType(namedFiles[item])[1]
		
		# Flatten any nested lists/tuples, and figure out the data types inside
		files = self.flatten(files, list)
		while len(files):
			filename, data = decideDataType(files.pop(0))
			if isinstance(data, dict):
				# User passed a dictionary-- 'filename' is unimportant, since this isn't a file
				for subName, subData in data.items():
					# We ignore the returned filename in the following assignment,
					# since the user has mapped a perferred filename
					namedFiles[self.createContentID(subName, False)] = decideDataType(subData)[1]
			elif not filename or not data:
				# Discard unknown data in the attachment list
				print 'Unrecognized entry: {filename} : {dataPreview}...'.format(filename=filename, dataPreview=repr(data)[:50])
			else:
				# Passed entry looks good, add it to our list of names/content-ids
				namedFiles[self.createContentID(filename, False)] = data
		
		# Now all files are in the 'namedFiles' dict (with unique names)
		for filename in namedFiles:
			data = namedFiles[filename]
			if isinstance(data, file):
				data = data.read()
			
			part = MIMEApplication(data)
			
			part.add_header('Content-Disposition', 'attachment; filename="{filename}"'.format(filename=filename))
			part.add_header('Content-ID', '<{cid}>'.format(cid=self.contentIDs[filename]))
			self.mail.attach(part)
		return self.contentIDs
	def addRecipients(self, *recipients, **options):
		"""	Used to add recipients to the email.
			Valid options are:
			* type - one of 'to', 'cc' or 'bcc'
			
			Preferred way to add recipients is to pass them during the ``send`` method.
		"""
		recipientList = None
		if 'type' not in options or options['type'] == 'to':
			recipientList = self.to
		elif options['type'] == 'cc':
			recipientList = self.cc
		elif options['type'] == 'bcc':
			recipientList = self.bcc
		
		recipients = self.flatten(recipients, lambda x: tuple(set(x)))
		recipientList.extend(recipients)
		return recipients
	def send(self, *recipients, **options):
		settings = {
			'server'				: 'smtp.gmail.com',
			'port'					: 587,
			'nickname'			: None,
			'tls'					: True,
			'to'					: [],
			'cc'					: [],
			'bcc'					: [],
			'strict_bcc' 		: False,
			'username'		: self.username,
			'password'			: self.password,
		}
		
		# Let the user override default settings:
		settings = dict(settings, **options)
		if 'sender' not in settings:
			settings['sender'] = '{user}@gmail.com'.format(user=settings['username']),

		
		settings['to'] = self.flatten(self.to + list(recipients), lambda x: list(set(x)))
		
		# Let strings through for the 'cc' and 'bcc' fields
		for recipientType in ('cc', 'bcc'):
			if not isinstance(settings[recipientType], (tuple, list)):
				settings[recipientType] = [settings[recipientType]]
		settings['cc'] = self.flatten(settings['cc'] + self.cc, lambda x: list(set(x)))
		settings['bcc'] = self.flatten(settings['bcc'] + self.bcc, lambda x: list(set(x)))
		
		if settings['nickname']:
			settings['sender'] = '{nickname}<{email}>'.format(nickname=unicode(settings['nickname']), email=settings['sender'])
		
		# Being mail creation
		self.mail['to'] = ','.join(settings['to'])
		self.mail['cc'] = ','.join(settings['cc'])
		# For now, we don't put a 'bcc' header in the message, or else it defeats the purpose of bcc :) 
		
		totalMailsSent = 0
		mailServer = smtplib.SMTP(settings['server'], settings['port'])
		try:
			# Log into remote smtp server
			if settings['tls']:
				try:
					mailServer.starttls()
					mailServer.login(settings['username'], settings['password'])
				except smtplib.SMTPAuthenticationError:
					print 'Authentication failure for user {user}'.format(user=settings['user'])
					return totalMailsSent
			
			# Send initial message to all normal and cc recipients
			try:
				mailServer.sendmail(settings['sender'], settings['to'] + settings['cc'], self.mail.as_string())
				totalMailsSent += 1
			except smtplib.SMTPRecipientsRefused as e:
				print 'Error sending to normal and cc recipients:\n{error}'.format(error=e.__dict__.items()[0])
				return totalMailsSent
			
			
			# Handle bcc recipients
			if settings['bcc']:
			
				if settings['strict_bcc']:
					# User wants to conceal bcc addresses from other bcc recipients.
					# This requires that each recipient receives a unique copy of the email
					# with the appropriate 'bcc' header with their email address in it.
					# Kind of a drain on resources if message has attachments.
					for recipient in settings['bcc']:
						if 'bcc' in self.mail:		self.mail.replace_header('bcc', recipient)
						else:							self.mail['bcc'] = recipient
						try:
							mailServer.sendmail(settings['sender'], [recipient], self.mail.as_string())
							totalMailsSent += 1
						except smtplib.SMTPRecipientsRefused as (e,reason):
							print 'Error sending to bcc recipient {recipient}: {error}'.format(recipient=repr(recipient), error=reason)
							return totalMailsSent
				else:
					# User wants to allow all bcc recipients to mutually see who other bcc recipients' addresses.
					recipients = ','.join(settings['bcc'])
					if 'bcc' in self.mail:			self.mail.replace_header('bcc', recipients)
					else:								self.mail['bcc'] = recipients
					try:
						mailServer.sendmail(settings['sender'], settings['bcc'], self.mail.as_string())
						totalMailsSent
					except smtplib.SMTPRecipientsRefused as (e,reason):
						print 'Error sending to bcc recipients: {error}'.format(error=reason)
						return totalMailsSent
			# Done
			return totalMailsSent
		finally:
			mailServer.quit()
	def flatten(self, iterable, return_as=None):
		flattened = list(iterable)
		i = 0
		while i < len(flattened):
			while isinstance(flattened[i], (list,tuple)):
				if not flattened[i]:
					flattened.pop(i)
					i -= 1
					break
				else:
					flattened[i:i+1] = flattened[i]
			i += 1
		
		if not return_as:
			return_as = type(iterable)
		return return_as(flattened)
